package com.cyys.modules.config;

import com.cyys.modules.config.filter.CustomShiroFilterFactoryBean;
import com.cyys.modules.core.listener.ShiroSessionListener;
import com.cyys.modules.core.shiro.token.UserRealm;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisClusterManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.*;

/**
 * shiro权限管理的配置
 *
 */

@Configuration
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.password}")
    private String password;

	/**
	 * 安全管理器
	 */
	@Bean(name="securityManager")
	public DefaultWebSecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(redisCacheManager());
//		securityManager.setRememberMeManager(rememberMeManager(rememberMeCookie()));
		return securityManager;
	}

	/**
	 * 项目自定义的Realm
	 */
	@Bean
	public UserRealm userRealm() {
		return new UserRealm();
	}

	/**
	 * rememberMe管理器, cipherKey生成见{@code Base64Test.java}
	 */
//	@Bean
//	public CookieRememberMeManager rememberMeManager(SimpleCookie rememberMeCookie) {
//		CookieRememberMeManager manager = new CookieRememberMeManager();
//		manager.setCipherKey(Base64.decode("fkffkkf44ovvjvdjwecc23323"));
//		manager.setCookie(rememberMeCookie);
//		return manager;
//	}

	/**
	 * 记住密码Cookie
	 */
//	@Bean
//	public SimpleCookie rememberMeCookie() {
//		SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//		simpleCookie.setHttpOnly(true);
//		simpleCookie.setMaxAge(7 * 24 * 60 * 60);// 7天
//		return simpleCookie;
//	}

	/**
	 * Shiro的过滤器链
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
		CustomShiroFilterFactoryBean shiroFilter = new CustomShiroFilterFactoryBean();
//		ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
		shiroFilter.setSecurityManager(securityManager);

		/**
		 * 默认的登陆访问url
		 */
		shiroFilter.setLoginUrl("/manage/tologin");
		/**
		 * 登陆成功后跳转的url
		 */
		shiroFilter.setSuccessUrl("/manage/toMain");
		/**
		 * 没有权限跳转的url
		 */
		shiroFilter.setUnauthorizedUrl("/unauthorized.jsp");
		/**
		 * 配置shiro拦截器链
		 *
		 * anon 不需要认证 authc 需要认证 user 验证通过或RememberMe登录的都可以
		 *
		 * 当应用开启了rememberMe时,用户下次访问时可以是一个user,但不会是authc,因为authc是需要重新认证的
		 *
		 * 顺序从上到下,优先级依次降低
		 *
		 */
		Map<String, String> hashMap = new LinkedHashMap<>();

		hashMap.put("/ueditor/**", "anon");
		hashMap.put("/files/**", "anon");
		hashMap.put("/static/**", "anon");

		hashMap.put("/general/**", "anon");
		hashMap.put("/manage/**", "anon");
		hashMap.put("/**", "authc");
		shiroFilter.setFilterChainDefinitionMap(hashMap);
		return shiroFilter;
	}

	/**
	 * 在方法中 注入 securityManager,进行代理控制
	 */
	@Bean
	public MethodInvokingFactoryBean methodInvokingFactoryBean(DefaultWebSecurityManager securityManager) {
		MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
		bean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
		bean.setArguments(new Object[] { securityManager });
		return bean;
	}

	/**
	 * Shiro生命周期处理器: 用于在实现了Initializable接口的Shiro
	 * bean初始化时调用Initializable接口回调(例如:UserRealm) 在实现了Destroyable接口的Shiro bean销毁时调用
	 * Destroyable接口回调(例如:DefaultSecurityManager)
	 */
	@Bean
	public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}

	/**
	 * 启用shrio授权注解拦截方式，AOP式方法级权限检查
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
			DefaultWebSecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}

	/**
	 * 配置保存sessionId的cookie
	 * 注意：这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
	 * 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid
	 * @return
	 */
	@Bean("sessionIdCookie")
	public SimpleCookie sessionIdCookie(){
		//这个参数是cookie的名称
		SimpleCookie simpleCookie = new SimpleCookie("sid");
		//setcookie的httponly属性如果设为true的话，会增加对xss防护的安全系数。它有以下特点：

		//setcookie()的第七个参数
		//设为true后，只能通过http访问，javascript无法访问
		//防止xss读取cookie
		simpleCookie.setHttpOnly(true);
		simpleCookie.setPath("/");
		//maxAge=-1表示浏览器关闭时失效此Cookie
		simpleCookie.setMaxAge(-1);
		return simpleCookie;
	}

	/**
	 * 配置session监听
	 * @return
	 */
	@Bean("sessionListener")
	public ShiroSessionListener sessionListener(){
		ShiroSessionListener sessionListener = new ShiroSessionListener();
		return sessionListener;
	}

	/**
	 * 配置会话管理器，设定会话超时及保存
	 * @return
	 */
	@Bean("sessionManager")
	public SessionManager sessionManager() {
		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		Collection<SessionListener> listeners = new ArrayList<SessionListener>();
		//配置监听
		listeners.add(sessionListener());
		sessionManager.setSessionListeners(listeners);

		sessionManager.setSessionIdCookie(sessionIdCookie());

		sessionManager.setSessionDAO(sessionDAO());
		sessionManager.setCacheManager(redisCacheManager());

		//全局会话超时时间（单位毫秒），默认30分钟  暂时设置为10秒钟 用来测试
		//8小时  28800000
		//30分钟 1800000
		sessionManager.setGlobalSessionTimeout(28800000);
		//是否开启删除无效的session对象  默认为true
		sessionManager.setDeleteInvalidSessions(true);
		//是否开启定时调度器进行检测过期session 默认为true
		sessionManager.setSessionValidationSchedulerEnabled(true);
		//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
		//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
		//暂时设置为 5秒 用来测试
		sessionManager.setSessionValidationInterval(14400000);
		//取消url 后面的 JSESSIONID
		sessionManager.setSessionIdUrlRewritingEnabled(false);
		return sessionManager;

	}


    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host + ":" + port);
//        redisManager.setPort(port);
		redisManager.setPassword(password);
        redisManager.setTimeout(timeout);
        return redisManager;
    }


	/**
	 * shiro缓存管理器;
	 * 需要添加到securityManager中
	 * @return
	 */
	@Bean("redisCacheManager")
	public RedisCacheManager redisCacheManager(){
		RedisCacheManager redisCacheManager = new RedisCacheManager();
		//redisCacheManager.setRedisManager(redisClusterManager());
		redisCacheManager.setRedisManager(redisManager());
		//redis中针对不同用户缓存
		redisCacheManager.setPrincipalIdFieldName("username");
		//用户权限信息缓存时间
		redisCacheManager.setExpire(30000000);
		return redisCacheManager;
	}

	/**
	 * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
	 * MemorySessionDAO 直接在内存中进行会话维护
	 * EnterpriseCacheSessionDAO  提供了缓存功能的会话维护，默认情况下使用MapCache实现，内部使用ConcurrentHashMap保存缓存的会话。
	 * @return
	 */
	@Bean
	public SessionDAO sessionDAO() {
		RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
		redisSessionDAO.setRedisManager(redisManager());
		//session在redis中的保存时间,最好大于session会话超时时间
		redisSessionDAO.setExpire(200000);
		return redisSessionDAO;
	}

	@Bean
	public RedisClusterManager redisClusterManager() {
		RedisClusterManager redisClusterManager = new RedisClusterManager();
		redisClusterManager.setHost("127.0.0.1:6379");
		return redisClusterManager;
	}

	/**
			* 解决： 无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 无效
     * shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter，
			* 只有perms，roles，ssl，rest，port才是属于AuthorizationFilter，而anon，authcBasic，auchc，user是AuthenticationFilter，
			* 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下，登录失败与没有权限都是通过抛出异常。
			* 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
     * @return
			 */
	@Bean
	public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
		SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
		Properties properties=new Properties();
		properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/manage/unauthorized");
		properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/manage/unauthorized");
		simpleMappingExceptionResolver.setExceptionMappings(properties);
		return simpleMappingExceptionResolver;
	}

	/**
	 * 解决spring-boot Whitelabel Error Page
	 * @return
	 */
	@Bean
	public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
		return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
			@Override
			public void customize(ConfigurableWebServerFactory factory) {
				ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/manage/unauthorized");
				ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/manage/404");
				ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/manage/500");
				HashSet<ErrorPage> set = new HashSet<>();
				set.add(error401Page);
				set.add(error404Page);
				set.add(error500Page);
				factory.setErrorPages(set);
			}
		};
	}
}
